/*
 * Copyright 2007 Rob Nielsen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.proxymatic.core;

import com.googlecode.proxymatic.core.builder.ProxyObjectBuilder;
import com.googlecode.proxymatic.core.engine.JavaProxyEngine;
import com.googlecode.proxymatic.core.util.ReflectUtil;
import com.googlecode.proxymatic.core.util.StringUtil;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This is the main class for creating proxy objects.  The main method is proxy(RuntimeContext) but a number of other
 * methods are provided for ease of use of calling code.  Callers of ProxyBuilder can make use of
 * AbstractProxyApplication as a simple framework for setting up the HandlerConfiguration.
 * <p/>
 * When one of the proxy methods is called for a given BuildtimeContext, the system first goes through each method in
 * the targetInterfaces and finds an appropriate MethodInvoker for that method.  If any methods cannot be handled an
 * exception will be thrown. This mapping is cached in the ProxyBuilder so subsequent calls with an equal
 * BuildtimeContext will skip this step.  For performance reasons it is beneficial to keep a ProxyBuilder, rather than
 * create a new one each time a proxy is required.
 */
public class ProxyBuilder implements Serializable {
    private transient Map<BuildtimeContext, Map<MethodSpec, MethodInvoker>> masterHandlerMap;
    private HandlerConfiguration handlerConfiguration;
    private ProxyEngine proxyEngine;

    /**
     * Constructs a new ProxyBuilder
     *
     * @param handlerConfiguration a set of handlers to be used when creating proxies.  This handler configuration will
     *                             be cloned so any changes to the handlers after construction will be ignored.
     */
    public ProxyBuilder(HandlerConfiguration handlerConfiguration) {
        this(handlerConfiguration, new JavaProxyEngine());
    }

    public ProxyBuilder(HandlerConfiguration handlerConfiguration, ProxyEngine proxyEngine) {
        this.handlerConfiguration = new HandlerConfiguration(handlerConfiguration);
        this.proxyEngine = proxyEngine;
    }

    /**
     * Returns a copy of the handlers used by this proxy builder.  Any changes to the handlers will be ignored.
     *
     * @return the handler configuration
     */
    public HandlerConfiguration getHandlerConfiguration() {
        return new HandlerConfiguration(handlerConfiguration);
    }

    public ProxyEngine getProxyEngine() {
        return proxyEngine;
    }

    /**
     * Ensures that a particular BuildtimeContext is compatible with the set of handlers available.  This will also add
     * the mappings to the cache so subsequent calls to proxy will be fast.
     *
     * @param context the targetInterfaces and implementationClasses (if appropriate)
     * @throws IllegalArgumentException if the targetInterfaces contains methods that cannot be handled.
     */
    public void ensureHandled(BuildtimeContext context) {
        getHandlerMap(context);
    }

    private Object createProxy(RuntimeContext runtimeContext) {
        BuildtimeContext buildtimeContext = runtimeContext.getBuildtimeContext();
        Map<MethodSpec, MethodInvoker> methodHandlerMap = getHandlerMap(buildtimeContext);
        List<ExceptionHandler> exceptionHandlers = handlerConfiguration.getExceptionHandlers();
        ProxyConfiguration proxyConfiguration = new ProxyConfiguration(runtimeContext, methodHandlerMap, exceptionHandlers, this);
        return proxyEngine.createProxy(proxyConfiguration);
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ProxyBuilder that = (ProxyBuilder) o;
        return handlerConfiguration.equals(that.handlerConfiguration);
    }

    public int hashCode() {
        return handlerConfiguration.hashCode();
    }

    public ProxyObjectBuilder proxy() {
        return new ProxyObjectBuilder();
    }

    public Object create(ProxyObjectBuilder proxyObjectBuilder) {
        return createProxy(proxyObjectBuilder.buildRuntimeContext());
    }

    private Map<MethodSpec, MethodInvoker> getHandlerMap(BuildtimeContext context) {
        Map<MethodSpec, MethodInvoker> handlerMap = findHandlerMap(context);
        if (handlerMap == null) {
            handlerMap = createHandlerMap(context);
        }
        return handlerMap;
    }

    private Map<MethodSpec, MethodInvoker> findHandlerMap(BuildtimeContext context) {
        return getMasterHandlerMap().get(context);
    }

    private Map<MethodSpec, MethodInvoker> createHandlerMap(BuildtimeContext context) {
        try {
            Map<MethodSpec, MethodInvoker> handlerMap = registerEmptyMap(context);
            Method[] objectMethods = {
                    ReflectUtil.getMethod(Object.class, "toString"),
                    ReflectUtil.getMethod(Object.class, "hashCode"),
                    ReflectUtil.getMethod(Object.class, "equals", Object.class)
            };
            registerMethods(objectMethods, context, handlerMap);
            for (Class interfaceToProxy : context.getTargetInterfaces()) {
                registerMethods(interfaceToProxy.getMethods(), context, handlerMap);
            }
            return handlerMap;
        } catch (Exception e) {
            throw new IllegalArgumentException("Can't create handler for " + StringUtil.sentence(context.getTargetInterfaces()) + " backed by " + StringUtil.sentence(context.getImplementationClasses()) + ":\n" + e.getMessage(), e);
        }
    }

    private void registerMethods(Method[] methods, BuildtimeContext context, Map<MethodSpec, MethodInvoker> handlerMap) {
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            MethodSpec spec = new MethodSpec(method);
            if (handlerMap.containsKey(spec)) {
                continue;
            }
            MethodInvoker methodInvoker = findMethodHandler(handlerConfiguration.getMethodHandlers(), method, context);
            checkExceptionHandlers(handlerConfiguration.getExceptionHandlers(), methodInvoker.getExceptionTypes(), method);
            checkExceptionHandlers(handlerConfiguration.getExceptionHandlers(), new Class[]{RuntimeException.class, Error.class}, method);
            handlerMap.put(spec, methodInvoker);
        }
    }

    private Map<MethodSpec, MethodInvoker> registerEmptyMap(BuildtimeContext context) {
        Map<MethodSpec, MethodInvoker> handlerMap = new HashMap<MethodSpec, MethodInvoker>();
        getMasterHandlerMap().put(context, handlerMap);
        return handlerMap;
    }

    private Map<BuildtimeContext, Map<MethodSpec, MethodInvoker>> getMasterHandlerMap() {
        if (masterHandlerMap == null) {
            masterHandlerMap = new HashMap<BuildtimeContext, Map<MethodSpec, MethodInvoker>>();
        }
        return masterHandlerMap;
    }

    private void checkExceptionHandlers(List<ExceptionHandler> exceptionHandlers, Class[] classes, Method method) {
        for (int i = 0; i < classes.length; i++) {
            checkExceptionHandlers(exceptionHandlers, method, classes[i]);
        }
    }

    private void checkExceptionHandlers(List<ExceptionHandler> exceptionHandlers, Method method, Class exceptionClass) {
        for (ExceptionHandler exceptionHandler : exceptionHandlers) {
            if (exceptionHandler.supportsException(exceptionClass, method)) {
                return;
            }
        }
        throw new IllegalArgumentException("exception");
    }

    private MethodInvoker findMethodHandler(List<MethodHandler> methodHandlerBuilders,
            Method method, BuildtimeContext context) {
        try {
            for (MethodHandler methodHandler : methodHandlerBuilders) {
                MethodInvoker methodInvoker = methodHandler.getInvoker(method, context, this);
                if (methodInvoker != MethodHandler.NOT_HANDLED) {
                    return methodInvoker;
                }
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("Can't find handler for " + StringUtil.shortMethod(method) + ":\n" + e.getMessage(), e);
        }
        throw new IllegalArgumentException("Can't find handler for " + StringUtil.shortMethod(method) + " using " + methodHandlerBuilders);
    }
}
